package com.zj.inoutput.service;

import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import com.github.pagehelper.PageInfo;
import com.github.pagehelper.page.PageMethod;
import com.zj.inoutput.common.ExportUtil;
import com.zj.inoutput.common.FileUtils;
import com.zj.inoutput.common.ResultData;
import com.zj.inoutput.common.constant.ExportStatus;
import com.zj.inoutput.common.constant.ExportType;
import com.zj.inoutput.dal.dataobj.Export;
import com.zj.inoutput.dal.dataobj.User;
import com.zj.inoutput.dal.mapper.ExportMapper;
import com.zj.inoutput.dal.mapper.UserMapper;
import com.zj.inoutput.model.request.ExportGetListParam;
import com.zj.inoutput.model.request.ExportParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.ListUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author DuanYu <a>Email:z-houjie@qq.com</a>
 * @date 2022-02-08 16:03
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ExportServiceImpl implements ExportService {

    private final ExportMapper exportMapper;

    @Resource(name = "asyncOutput")
    private AsyncTaskExecutor asyncOutput;

    private final UserMapper userMapper;

    /**
     * 文件上传
     *
     * @param file 文件
     */
    @Override
    public void upload(MultipartFile file) {
        try {
            // 这里 需要指定读用哪个class去读,然后读取第一个sheet,从第二行开始;文件流会自动关闭
            EasyExcelFactory.read(file.getInputStream(), User.class, new AnalysisEventListener<User>() {
                @Resource
                private UserMapper userMapper;

                /**
                 * 每隔500条存储数据库，然后清理list ，方便内存回收
                 */
                private static final int BATCH_COUNT = 500;

                private List<User> cachedDataList = new ArrayList<>(BATCH_COUNT);

                /**
                 * 每一条数据解析都调用一次这个方法
                 *
                 * @param user            用户数据
                 * @param analysisContext 读取excel信息
                 */
                @Override
                public void invoke(User user, AnalysisContext analysisContext) {
                    log.info("解析到一条数据:{}", JSON.toJSONString(user));
                    cachedDataList.add(user);
                    // 达到BATCH_COUNT了，需要去存储一次数据库，防止数据几万条数据在内存，容易OOM
                    if (cachedDataList.size() >= BATCH_COUNT) {
                        saveData();
                        // 存储完成清理 list
                        cachedDataList = new ArrayList<>(BATCH_COUNT);
                    }
                }

                /**
                 * 所有数据解析完成了调用这个方法
                 *
                 * @param analysisContext 读取excel信息
                 */
                @Override
                public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                    // 这里也要保存数据，将最后一批数据也存储到数据库
                    saveData();
                    log.info("所有数据解析完成");
                }

                private void saveData() {
                    log.info("{}条数据，开始存储数据库", cachedDataList.size());
                    userMapper.batchInsert(cachedDataList);
                    log.info("存储数据库成功");
                }
            }).sheet(0).headRowNumber(1).doRead();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }
    }

    /**
     * 获取下载列表
     *
     * @param param 查询条件
     * @return 分页列表
     */
    @Override
    public PageInfo<Export> findExportList(ExportGetListParam param) {
        PageMethod.startPage(param.getPageIndex(), param.getPageSize());
        return new PageInfo<>(exportMapper.selectByTopic(param.getTopic()));
        // 可以完善 操作人ID字段,再查询映射出操作人名称(举一反三)
    }

    /**
     * 下载导出文件
     *
     * @param id       导出记录ID
     * @param response 响应体
     */
    @Override
    public void download(Integer id, HttpServletResponse response) throws IOException {
        Export export = exportMapper.selectByPrimaryKey(id);
        if (Objects.nonNull(export) && Objects.equals(ExportStatus.SUCCESS.getValue(), export.getStatus())) {
            // 获取片段内容
            List<Integer> sliceIds = Arrays.stream(export.getInfoSlicingPath().split("-")).map(Integer::valueOf).collect(Collectors.toList());
            ExportTopic topic = getTopic(export.getExportTopic());
            List<String> slices = exportMapper.selectSliceInfoByLocation(topic.getLocation(), sliceIds);
            String join = String.join("", slices);
            List targetList;
            if (!StringUtils.hasText(join)) {
                log.info("数据为空");
                targetList = new ArrayList();
            } else {
                targetList = ExportUtil.deserialize(join, topic.getTarget());
            }
            ExportUtil.exportSingleSheetToHttpResponse(topic.getTarget(), () -> targetList, ResultData.failed("导出失败"), topic.getFileName(), "1", response);
        }
        throw new IllegalStateException("无可下载数据");
    }


    /**
     * 查询数据导出数据
     *
     * @param exportParam 查询参数
     */
    @Override
    public void exportExcel(ExportParam exportParam) {
        ExportTopic exportTopic = getTopic(exportParam.getTopic());
        // 生成导出记录
        Export export = new Export();
        export.setStatus(ExportStatus.EXPORTING.getValue());
        export.setExportTopic(exportTopic.getValue());
        export.setExportParameters(exportParam.getJsonParam());
        export.setExportType(ExportType.EXCEL.getValue());
        export.setCreatedTime(new Date());
        exportMapper.insertSelective(export);

        // 异步制作导出内容
        asyncOutput.execute(() -> {
            try {
                Object param = JSON.parseObject(exportParam.getJsonParam(), exportTopic.getParam());

                // 执行数据查询方法拿到数据集合
                List<?> data = exportTopic.getFunction().apply(param);
                // 分片存储
                String locations = dataInfoSlicing(exportTopic.getLocation(), data);
                // 更新导出情况
                exportMapper.updateByPrimaryKeySelective(new Export().setId(export.getId()).setInfoSlicingPath(locations).setStatus(ExportStatus.SUCCESS.getValue()));

                log.info("制作成功！地址库:{},分片ID：{}", exportTopic.getLocation(), locations);
            } catch (Exception e) {
                exportMapper.updateByPrimaryKeySelective(new Export().setId(export.getId()).setStatus(ExportStatus.FAIL.getValue()));
                log.error("制作异常：", e);
            }
        });
    }


    /**
     * 数据分片存储
     *
     * @param location 地址库
     * @param dataInfo 数据信息
     */
    private String dataInfoSlicing(String location, List<?> dataInfo) {
        // 内容压缩
        byte[] serialize = ExportUtil.serialize(dataInfo);
        // 内容分片
        List<String> slicingList = ExportUtil.slicing(serialize);
        // 存储 
        if (!CollectionUtils.isEmpty(slicingList)) {
            List<Map<String, Object>> exportInfo = slicingList.stream().map(v -> {
                Map<String, Object> infoMap = new HashMap<>(slicingList.size());
                infoMap.put("exportInfo", v);
                return infoMap;
            }).collect(Collectors.toList());

            // 在没有IO限制的条件下可以优化成并发写入
            exportMapper.insertSlice(exportInfo, location);
            return exportInfo.stream().map(v -> String.valueOf(v.get("id"))).collect(Collectors.joining("-"));
        }
        throw new IllegalStateException("数据分片存储异常");
    }

    /**
     * 获取导出主题
     *
     * @param topic 主题value
     * @return 主题
     */
    private ExportTopic getTopic(Integer topic) {
        return Objects.requireNonNull(ExportTopic.INSTANCE.get(topic), "不支持的导出主题");
    }
}
